summaryrefslogtreecommitdiff
path: root/app/[lng]/partners/data-room/[projectId]/settings/page.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-10-15 12:52:11 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-10-15 12:52:11 +0000
commitb54f6f03150dd78d86db62201b6386bf14b72394 (patch)
treeb3092bb34805fdc65eee5282e86a9fb90ba20d6e /app/[lng]/partners/data-room/[projectId]/settings/page.tsx
parentc1bd1a2f499ee2f0742170021b37dab410983ab7 (diff)
(대표님) 커버, 데이터룸, 파일매니저, 담당자할당 등
Diffstat (limited to 'app/[lng]/partners/data-room/[projectId]/settings/page.tsx')
-rw-r--r--app/[lng]/partners/data-room/[projectId]/settings/page.tsx488
1 files changed, 488 insertions, 0 deletions
diff --git a/app/[lng]/partners/data-room/[projectId]/settings/page.tsx b/app/[lng]/partners/data-room/[projectId]/settings/page.tsx
new file mode 100644
index 00000000..aa0f3b52
--- /dev/null
+++ b/app/[lng]/partners/data-room/[projectId]/settings/page.tsx
@@ -0,0 +1,488 @@
+
+// app/projects/[projectId]/settings/page.tsx
+'use client';
+
+import { useState, useEffect } from 'react';
+import {
+ Settings,
+ Shield,
+ Globe,
+ Trash2,
+ AlertCircle,
+ Save,
+ Lock,
+ Unlock,
+ Archive,
+ Users,
+ HardDrive
+} from 'lucide-react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Switch } from '@/components/ui/switch';
+import { Textarea } from '@/components/ui/textarea';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { useToast } from '@/hooks/use-toast';
+import { useRouter } from 'next/navigation';
+
+interface ProjectSettings {
+ id: string;
+ name: string;
+ description: string;
+ isPublic: boolean;
+ externalAccessEnabled: boolean;
+ storageLimit: number;
+ maxFileSize: number;
+ allowedFileTypes: string[];
+ autoArchiveDays: number;
+ requireApproval: boolean;
+ defaultCategory: string;
+}
+
+export default function ProjectSettingsPage({
+ params
+}: {
+ params: { projectId: string }
+}) {
+ const [settings, setSettings] = useState<ProjectSettings | null>(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [archiveDialogOpen, setArchiveDialogOpen] = useState(false);
+ const [currentUserRole, setCurrentUserRole] = useState<string>('viewer');
+
+ const { toast } = useToast();
+ const router = useRouter();
+
+ useEffect(() => {
+ fetchSettings();
+ checkUserRole();
+ }, [params.projectId]);
+
+ const fetchSettings = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch(`/api/projects/${params.projectId}/settings`);
+
+ if (!response.ok) {
+ throw new Error('설정을 불러올 수 없습니다');
+ }
+
+ const data = await response.json();
+ setSettings(data);
+ } catch (error) {
+ toast({
+ title: '오류',
+ description: '프로젝트 설정을 불러올 수 없습니다.',
+ variant: 'destructive',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const checkUserRole = async () => {
+ try {
+ const response = await fetch(`/api/projects/${params.projectId}/access`);
+ const data = await response.json();
+ setCurrentUserRole(data.role);
+ } catch (error) {
+ console.error('권한 확인 실패:', error);
+ }
+ };
+
+ const saveSettings = async () => {
+ if (!settings) return;
+
+ try {
+ setSaving(true);
+ const response = await fetch(`/api/projects/${params.projectId}/settings`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(settings),
+ });
+
+ if (!response.ok) throw new Error('설정 저장 실패');
+
+ toast({
+ title: '성공',
+ description: '프로젝트 설정이 저장되었습니다.',
+ });
+ } catch (error) {
+ toast({
+ title: '오류',
+ description: '설정 저장에 실패했습니다.',
+ variant: 'destructive',
+ });
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const deleteProject = async () => {
+ try {
+ const response = await fetch(`/api/projects/${params.projectId}`, {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) throw new Error('프로젝트 삭제 실패');
+
+ toast({
+ title: '성공',
+ description: '프로젝트가 삭제되었습니다.',
+ });
+
+ router.push('/projects');
+ } catch (error) {
+ toast({
+ title: '오류',
+ description: '프로젝트 삭제에 실패했습니다.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ const archiveProject = async () => {
+ try {
+ const response = await fetch(`/api/projects/${params.projectId}/archive`, {
+ method: 'POST',
+ });
+
+ if (!response.ok) throw new Error('프로젝트 보관 실패');
+
+ toast({
+ title: '성공',
+ description: '프로젝트가 보관되었습니다.',
+ });
+
+ router.push('/projects');
+ } catch (error) {
+ toast({
+ title: '오류',
+ description: '프로젝트 보관에 실패했습니다.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ const canEdit = currentUserRole === 'owner' || currentUserRole === 'admin';
+
+ if (loading || !settings) {
+ return (
+ <div className="p-6">
+ <div className="animate-pulse space-y-4">
+ {[...Array(5)].map((_, i) => (
+ <div key={i} className="h-20 bg-gray-200 rounded" />
+ ))}
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="p-6 space-y-6 max-w-4xl">
+ {/* 헤더 */}
+ <div className="flex items-center justify-between">
+ <div>
+ <h1 className="text-2xl font-bold">프로젝트 설정</h1>
+ <p className="text-muted-foreground mt-1">
+ 프로젝트 설정을 관리합니다
+ </p>
+ </div>
+
+ {canEdit && (
+ <Button onClick={saveSettings} disabled={saving}>
+ <Save className="h-4 w-4 mr-2" />
+ {saving ? '저장 중...' : '변경사항 저장'}
+ </Button>
+ )}
+ </div>
+
+ {!canEdit && (
+ <Alert>
+ <AlertCircle className="h-4 w-4" />
+ <AlertDescription>
+ 프로젝트 설정을 변경하려면 Owner 또는 Admin 권한이 필요합니다.
+ </AlertDescription>
+ </Alert>
+ )}
+
+ <Tabs defaultValue="general">
+ <TabsList>
+ <TabsTrigger value="general">일반</TabsTrigger>
+ <TabsTrigger value="access">접근 관리</TabsTrigger>
+ <TabsTrigger value="storage">스토리지</TabsTrigger>
+ {currentUserRole === 'owner' && (
+ <TabsTrigger value="danger">위험 영역</TabsTrigger>
+ )}
+ </TabsList>
+
+ <TabsContent value="general" className="space-y-4">
+ <Card>
+ <CardHeader>
+ <CardTitle>기본 정보</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div>
+ <Label htmlFor="name">프로젝트 이름</Label>
+ <Input
+ id="name"
+ value={settings.name}
+ onChange={(e) => setSettings({ ...settings, name: e.target.value })}
+ disabled={!canEdit}
+ />
+ </div>
+
+ <div>
+ <Label htmlFor="description">설명</Label>
+ <Textarea
+ id="description"
+ value={settings.description}
+ onChange={(e) => setSettings({ ...settings, description: e.target.value })}
+ disabled={!canEdit}
+ rows={3}
+ />
+ </div>
+
+ <div>
+ <Label htmlFor="category">기본 파일 카테고리</Label>
+ <Select
+ value={settings.defaultCategory}
+ onValueChange={(value) => setSettings({ ...settings, defaultCategory: value })}
+ disabled={!canEdit}
+ >
+ <SelectTrigger>
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="public">Public - 공개</SelectItem>
+ <SelectItem value="restricted">Restricted - 제한</SelectItem>
+ <SelectItem value="confidential">Confidential - 기밀</SelectItem>
+ <SelectItem value="internal">Internal - 내부</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+ </CardContent>
+ </Card>
+ </TabsContent>
+
+ <TabsContent value="access" className="space-y-4">
+ <Card>
+ <CardHeader>
+ <CardTitle>접근 설정</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="flex items-center justify-between">
+ <div>
+ <Label htmlFor="public">공개 프로젝트</Label>
+ <p className="text-sm text-muted-foreground">
+ 모든 사용자가 이 프로젝트를 볼 수 있습니다
+ </p>
+ </div>
+ <Switch
+ id="public"
+ checked={settings.isPublic}
+ onCheckedChange={(checked) => setSettings({ ...settings, isPublic: checked })}
+ disabled={!canEdit}
+ />
+ </div>
+
+ <div className="flex items-center justify-between">
+ <div>
+ <Label htmlFor="external">외부 사용자 접근 허용</Label>
+ <p className="text-sm text-muted-foreground">
+ 파트너사 사용자도 접근할 수 있습니다
+ </p>
+ </div>
+ <Switch
+ id="external"
+ checked={settings.externalAccessEnabled}
+ onCheckedChange={(checked) =>
+ setSettings({ ...settings, externalAccessEnabled: checked })
+ }
+ disabled={!canEdit}
+ />
+ </div>
+
+ <div className="flex items-center justify-between">
+ <div>
+ <Label htmlFor="approval">멤버 승인 필요</Label>
+ <p className="text-sm text-muted-foreground">
+ 새 멤버 참여 시 관리자 승인이 필요합니다
+ </p>
+ </div>
+ <Switch
+ id="approval"
+ checked={settings.requireApproval}
+ onCheckedChange={(checked) =>
+ setSettings({ ...settings, requireApproval: checked })
+ }
+ disabled={!canEdit}
+ />
+ </div>
+ </CardContent>
+ </Card>
+ </TabsContent>
+
+ <TabsContent value="storage" className="space-y-4">
+ <Card>
+ <CardHeader>
+ <CardTitle>스토리지 설정</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div>
+ <Label htmlFor="storage-limit">스토리지 제한 (GB)</Label>
+ <Input
+ id="storage-limit"
+ type="number"
+ value={settings.storageLimit}
+ onChange={(e) => setSettings({
+ ...settings,
+ storageLimit: parseInt(e.target.value)
+ })}
+ disabled={!canEdit}
+ />
+ </div>
+
+ <div>
+ <Label htmlFor="file-size">최대 파일 크기 (MB)</Label>
+ <Input
+ id="file-size"
+ type="number"
+ value={settings.maxFileSize}
+ onChange={(e) => setSettings({
+ ...settings,
+ maxFileSize: parseInt(e.target.value)
+ })}
+ disabled={!canEdit}
+ />
+ </div>
+
+ <div>
+ <Label htmlFor="auto-archive">자동 보관 (일)</Label>
+ <Input
+ id="auto-archive"
+ type="number"
+ value={settings.autoArchiveDays}
+ onChange={(e) => setSettings({
+ ...settings,
+ autoArchiveDays: parseInt(e.target.value)
+ })}
+ disabled={!canEdit}
+ />
+ <p className="text-sm text-muted-foreground mt-1">
+ 설정한 기간 동안 접근하지 않은 파일을 자동으로 보관합니다
+ </p>
+ </div>
+ </CardContent>
+ </Card>
+ </TabsContent>
+
+ {currentUserRole === 'owner' && (
+ <TabsContent value="danger" className="space-y-4">
+ <Card className="border-red-200">
+ <CardHeader>
+ <CardTitle className="text-red-600">위험 영역</CardTitle>
+ <CardDescription>
+ 이 작업들은 되돌릴 수 없습니다. 신중하게 진행하세요.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="flex items-center justify-between p-4 border rounded-lg">
+ <div>
+ <h3 className="font-medium">프로젝트 보관</h3>
+ <p className="text-sm text-muted-foreground">
+ 프로젝트를 보관하면 읽기 전용이 됩니다
+ </p>
+ </div>
+ <Button
+ variant="outline"
+ onClick={() => setArchiveDialogOpen(true)}
+ >
+ <Archive className="h-4 w-4 mr-2" />
+ 프로젝트 보관
+ </Button>
+ </div>
+
+ <div className="flex items-center justify-between p-4 border rounded-lg border-red-200">
+ <div>
+ <h3 className="font-medium text-red-600">프로젝트 삭제</h3>
+ <p className="text-sm text-muted-foreground">
+ 프로젝트와 모든 파일을 영구적으로 삭제합니다
+ </p>
+ </div>
+ <Button
+ variant="destructive"
+ onClick={() => setDeleteDialogOpen(true)}
+ >
+ <Trash2 className="h-4 w-4 mr-2" />
+ 프로젝트 삭제
+ </Button>
+ </div>
+ </CardContent>
+ </Card>
+ </TabsContent>
+ )}
+ </Tabs>
+
+ {/* 삭제 확인 다이얼로그 */}
+ <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>프로젝트 삭제</DialogTitle>
+ <DialogDescription className="text-red-600">
+ 정말로 이 프로젝트를 삭제하시겠습니까?
+ 모든 파일과 데이터가 영구적으로 삭제됩니다.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter>
+ <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+ 취소
+ </Button>
+ <Button variant="destructive" onClick={deleteProject}>
+ 삭제
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
+ {/* 보관 확인 다이얼로그 */}
+ <Dialog open={archiveDialogOpen} onOpenChange={setArchiveDialogOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>프로젝트 보관</DialogTitle>
+ <DialogDescription>
+ 프로젝트를 보관하시겠습니까?
+ 보관된 프로젝트는 읽기 전용이 되며, 언제든지 복원할 수 있습니다.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter>
+ <Button variant="outline" onClick={() => setArchiveDialogOpen(false)}>
+ 취소
+ </Button>
+ <Button onClick={archiveProject}>
+ 보관
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </div>
+ );
+}